package dapp

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"

	"github.com/spf13/cobra"
	"hundsun.com/hsl/hschain/cmd/cli/common"
	"hundsun.com/hsl/hschain/common/crypto"
	"hundsun.com/hsl/hschain/common/keystore"
	"hundsun.com/hsl/hschain/common/types"
	"hundsun.com/hsl/hschain/common/util"
	"hundsun.com/hsl/hschain/protos"
	"hundsun.com/hsl/hschain/protos/config"
	protosrpc "hundsun.com/hsl/hschain/protos/rpc"
	service "hundsun.com/hsl/hschain/rpc/impl"
)

var eccCryptoConfig = &config.CryptoConfig{
	AsymmetricAlgorithm: "ECDSA",
	HashFamily:          "SHA256",
	SymmetricAlgorithm:  "AES",
}

var gmCryptoConfig = &config.CryptoConfig{
	AsymmetricAlgorithm: "SM2",
	HashFamily:          "SM3",
	SymmetricAlgorithm:  "SM4",
}

//AccountNumMax ...
var AccountNumMax = 100000

type cryptoFamilyType int8

const (
	//CryptoFamilyEcc ecc crypto family
	CryptoFamilyEcc cryptoFamilyType = iota
	//CryptoFamilyGm gm crypto family
	CryptoFamilyGm
)

var cryptoFamilyName = map[cryptoFamilyType]string{
	CryptoFamilyEcc: "ECC",
	CryptoFamilyGm:  "GM",
}

// GetCryptoString get crypto string
func GetCryptoString(cryptoType cryptoFamilyType) string {
	if cryptoStr, ok := cryptoFamilyName[cryptoType]; ok {
		return cryptoStr
	}
	return ""
}

// AccountCmd net command
func AccountCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "account",
		Short: "Account operation",
		Args:  cobra.MinimumNArgs(1),
	}

	cmd.AddCommand(
		GetAccountBalanceCmd(),
		generateKeystoreCmd(),
		listAccountsCmd(),
	)
	return cmd
}

//GetAccountBalanceCmd query account's balance from chain
func GetAccountBalanceCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "balance",
		Short: "Get account balance",
		Run:   accountBalance,
	}
	cmd.Flags().StringP("account", "a", "", "account address")
	cmd.MarkFlagRequired("account")

	return cmd
}

func accountBalance(cmd *cobra.Command, args []string) {
	addr, _ := cmd.Flags().GetString("account")
	rpcLaddr, _ := cmd.Flags().GetString(common.FlagRPCLaddr)
	param := &protosrpc.GetBalanceParam{
		Address: addr,
	}

	common.RPCCall(rpcLaddr, service.GetBalance, param, nil)
}

func listAccountsCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "list",
		Short: "List keystore accounts",
		Long:  "List keystore accounts",
		Run:   listAccounts,
	}

	cmd.Flags().StringP("keystore", "f", "keystore.json", "keystore file path")
	return cmd
}

func listAccounts(cmd *cobra.Command, args []string) {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()
	file, _ := cmd.Flags().GetString("keystore")
	keystoreBytes, err := util.ReadFile(file)
	if err != nil {
		fmt.Printf("read key store file failed, err: %s\n", err.Error())
		return
	}
	var accountInfo = &types.EncryptedAccountInfo{}
	err = json.Unmarshal(keystoreBytes, accountInfo)
	if err != nil {
		fmt.Printf("marshal json failed, err: %s\n", err.Error())
		return
	}

	fmt.Printf("encrypt: %s\n", accountInfo.EncryptType)
	var addresses []string
	for _, account := range accountInfo.AccountList {
		addresses = append(addresses, account.Address)
	}
	fmt.Printf("addresses:%v\n", addresses)
}

func generateKeystoreCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "generate",
		Short: "Generate keystore file",
		Long:  "Generate keystore file",
		Run:   generate,
	}

	cmd.Flags().Uint16P("quantity", "q", 1, "number of account to generate")
	cmd.Flags().StringP("password", "p", "123456", "password")
	cmd.Flags().StringP("output", "o", "keystore.json", "keystore output file")
	cmd.Flags().IntP("cryptotype", "t", 0, "encryption algorithm 0:ECC 1:GM (default 0)")
	return cmd
}

func generate(cmd *cobra.Command, args []string) {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()
	qty, _ := cmd.Flags().GetUint16("quantity")
	passwd, _ := cmd.Flags().GetString("password")
	if common.IsChineseChar(passwd) {
		fmt.Printf("password failed, reason:%s\n", errors.New("invalid input"))
		return
	}
	output, _ := cmd.Flags().GetString("output")
	cryptoType, _ := cmd.Flags().GetInt("cryptotype")

	var err error
	switch cryptoFamilyType(cryptoType) {
	case CryptoFamilyEcc:
		err = eccCryptoConfig.Validate()
	case CryptoFamilyGm:
		err = gmCryptoConfig.Validate()
	default:
		err = errors.New("invalid option")
	}
	if err != nil {
		fmt.Printf("crypto failed, reason:%s\n", err)
		return
	}

	//判断文件路径是否存在
	bol := util.FileExists(output)

	var accountOutput types.EncryptedAccountInfo
	var accountInput types.EncryptedAccountInfo
	var addedAccounts []types.EncryptedAccount
	var addedAddress []string

	if bol {
		//验证密码是否正确
		_, err := keystore.LoadKeyStore(output, passwd)
		if err != nil {
			fmt.Printf("Load key store failed, err: %s\n", err.Error())
			return
		}
		//读取原有账户
		keystoreBytes, err := util.ReadFile(output)
		if err != nil {
			fmt.Printf("Read key store failed, err: %s\n", err.Error())
			return
		}
		err = json.Unmarshal(keystoreBytes, &accountInput)
	}

	var i = uint16(0)

	for ; i < qty; i++ {
		kp, err := crypto.KeyPairGen()
		if err != nil {
			fmt.Println(err)
			return
		}
		pubKeyBytes := kp.PublicKey().Bytes()
		address, err := protos.ParseAddressFromPublicKeyBytes(pubKeyBytes)
		if err != nil {
			fmt.Println(err)
			return
		}
		privKeyBytes := kp.Bytes()
		hash, _ := crypto.Hash([]byte(passwd))
		key, _ := crypto.ParserSymmetricKey(hash)
		encryptedPrivKeyBytes, err := crypto.Encrypt(key, privKeyBytes)
		if err != nil {
			fmt.Println(err)
			return
		}

		if len(accountInput.AccountList) >= AccountNumMax {
			fmt.Printf("account num exceed %d\n", AccountNumMax)
			break
		}
		addedAccounts = append(addedAccounts, types.EncryptedAccount{
			Address:    string(address),
			Ciphertext: hex.EncodeToString(encryptedPrivKeyBytes),
		})
		addedAddress = append(addedAddress, string(address))
	}

	accountInput.AccountList = append(accountInput.AccountList, addedAccounts...)

	accountOutput.EncryptType = GetCryptoString(cryptoFamilyType(cryptoType))
	accountOutput.AccountList = accountInput.AccountList

	accountsBytes, err := json.Marshal(accountOutput)
	if err != nil {
		fmt.Println(err)
		return
	}
	err = ioutil.WriteFile(output, accountsBytes, 0600)
	if err != nil {
		fmt.Printf("output account failed, reason:%s, file path:%s\n", err, output)
		return
	}
	fmt.Printf("added addresses:%v\n", addedAddress)
}
